Docker Autobahn環境 + wss + BouncyCastle C#
概要
これ
が動く環境をDockerで作って、適当に動かす。主にTls周りとか。
C#側はWebSocket(生ソケットで遊ぼうの会) + BouncyCastle解読回。
DockerでのAutobahn動作環境の作成から、C#でのBouncyCastleの使い方発掘、TLS通してHandshakeまでをやる羽目になった。
動機
自作のC# WebSocketライブラリ「WebuSocket」で、WebSocketのテストちゃんと通したことそういえば無いよねっていう感じ。
準備
docker pull python
これで入る環境ってどんなのなんだろう。Python3だと嬉しいんだけど。
-> 3.5だった。やったね!
container立ち上げて
docker run -ti -d --name python python:latest
bash起動して入る
docker exec -ti python bash
Autobahn動かす
TLSで動くサンプルがあるんで、これが動かせれば行けそう。
適当にソースをホストに置いて、動かせるようにしてみよう。
docker run -ti -d --name python -v /Users/tartetatin/Desktop/pythonServer:/usr/local/src python:latest
Autobahnの備え付けのサーバを動かすために、いろいろ依存が足りないので入れる。
pip install autobahn
pip install twisted
pip install pyopenssl
pip install txaio
pip install service_identity
まだport開けてなかった、、一度イメージに落とすのがいいのかな、、commitしちゃおう、、
docker commit python autobahn:latest
というわけで、ホストから接続
docker run -ti -p 8000:8000 -p 9000:9000 -d --name autobahn -v /Users/tartetatin/Desktop/pythonServer:/usr/local/src autobahn:latest
8000番はHTTP、9000番はWebSocketの受け口にしてる。
で、
ブラウザで8000番を開くと、certificate入れなよ~っていうリンクを示してくれる。
そのままブラウザで9000番を開くと、ここはWebSocketのEndPointだよ~って言われる。
で。
ブラウザで適当なWebSocketクライアント書いて接続をwss://127.0.0.1:9000で行うと、 OSStatus Error -9807: Invalid certificate chain が出る。 ふむ。まあ確かに。オレオレ証明書だし。
ブラウザを説得するのは諦めて、C#のSocketでの接続を考える。
ここで問題にぶち当たる。
SslStreamを使えばTLSでAutobahnとの接続はできるんだけど、これTLS1.0じゃん。あとSslStreamねぇ、、、
C#というか。.Net FrameworkにはSslStreamという、SSL系の動作をhandshakeからsend/receiveまでまるっと飲み込んだAPIが存在する。
存在するんだけどさ~~~。必ずNetworkStreamを使う実装になっちゃう。
で、作ってるWebuSocketっていうWebSocketクライアントは、Socketを使っていろいろやってるので、NetworkStreamに落ちたら負けだと思って(ry
SocketからNetworkStreamを取り出してSslStreamに繋ぐ、とかはまあ、超簡単にできるんだけど、StreamのAPIが気に食わん。
非同期API使うに決まってるんだけど、Task使うんで必ずThreadできちゃうし。
Socketでなんとかしたいな~ってなって、いろんな方向を探した。
ぶっちゃけやりたいこと
・TLS protocolでのサーバとのやりとりは、ざっくり言うとそういう動作をする state machine が手にはいればよい。
・インターフェースとして、TCPで取得したbyte[]を放り込むと、TLSのhandshakeをしたり、handshake済みであればdecryptedなbyte[]を吐き出す機構がある、、はず
で、探したら.Net Frameworkにはそういうのなかった。
困ったことに、.Net Frameworkには、TLSを扱えるstate machine が無い。
JavaでいうSSLEngineみたいなやつ。
なんでねーーーんだろう。SslStreamでみんな満足してんの? 本当?
で、いろいろ探し回って、OpenSSLのC#ラッパ見たり、オープンソースな何かとか見てて、
最終的にBouncyCastleにたどり着いた。
そんなBouncyCastle
https://www.bouncycastle.org/csharp/index.html
github上にもある。
https://github.com/bcgit/bc-csharp
、、、信じられないことにAPI documentが無いんだよね。なぜだろうね。
testとか読むことになった。
というわけでBouncyCastleを読む
まずは関係ありそうなTlsTestCase.csを見てみる。
https://github.com/bcgit/bc-csharp/blob/master/crypto/test/src/crypto/tls/test/TlsTestCase.cs
いろいろあるな~~楽しそうなの。
WriteHandshakeMessage
blockingモードでのみ使えるStreamとか
public virtual void OfferInput(byte[] input) こっちはノンブロッキング前提でしか使えないらしい。データを送るっぽい。
public virtual void OfferOutput(byte[] buffer, int offset, int length) データを受け取るっぽい。
-> streamを引数に持つTlsClientProtocolコンストラクタは全部ブロッキングで、
SecureRandomだけを引数に持つTlsClientProtocolコンストラクタがノンブロッキングだった。
-> そもそもブロッキングモードってなんなの?
あとでわかることだが、streamを使った入出力隠蔽ができているものをblocking、
streamを使わず自分でbyte[]入れたり出したりできるstate machineとしての利用法のことをnon-blocking と呼んでいるようだった。
それはblockingという概念だろうか、、、
どうやら、俺が欲しいものはnon-blocking modeらしい。
これとりあえずどうやってip/port指定するんだろう。
-> byte[]でのインプット、アウトプットを与えると、内部状態が勝手に遷移するっていうスーパートンチキマシンだこれ。
、、たのむドキュメントに書いててくれ。最高だぞこの機能。っていうかなんでC#オリジナルには存在しないんだよこういうの。
(SslStreamはstreamになっちゃうのでこの辺隠蔽できてるんだけど、隠蔽しすぎててsocketと併用できない。
test codeから使い方を読み解く
この辺のコードが参考になっ、、たらよかったんだけど。
このコードでしかnon-blocking mode が出てきてなかったので、これしか参考になるものがなかったと言ったほうがいい。
BouncyCastleのAPIを使ったサンプル
C#のSocketでの接続から送受信まで、まとめて書く。
本当にわかりにくいAPIだった。こういうテストかドキュメントがあればめっちゃ簡単に済んだ。
クライアント側のコードで、Socketを使ってサーバに接続、TLSでのnegoとかして、TLS handshakeが終わるところまでのサンプルが下記。
おまけで、handshake後にデータの送受信をしている。
セキュリティ的にはガバガバなので、まんま使って死なないように気をつけてね。
クライアント側の前提
・DefaultTlsClientを拡張したMyTlsClientクラスを定義してある。(独自のTlsAuthenticationを返すメソッドを実装する必要があるので定義必須)
・MyTlsClient内に、MyTlsAuthenticationクラスを定義してある。(TlsAuthenticationがabstractなので実装必須)
サーバ側の前提
・ここでテストに使ったサーバは、TLS1.2が喋れるサーバ。
・サーバは、handshakeが終わった後にクライアントから送られてきたデータがあれば、そのままクライアントに返すという仕様。
using Org.BouncyCastle.Crypto.Tls;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class MyTlsClient : DefaultTlsClient {
internal TlsSession mSession;
internal WebuSocketTlsClient (TlsSession session) {
this.mSession = session;
}
public override TlsSession GetSessionToResume () {
return this.mSession;
}
public override void NotifyAlertRaised (byte alertLevel, byte alertDescription, string message, Exception cause) {
Debug.LogError("TLS client raised alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription));
if (message != null) {
Debug.LogError("> " + message);
}
if (cause != null) {
Debug.LogError(cause);
}
}
public override void NotifyAlertReceived (byte alertLevel, byte alertDescription) {
Debug.LogError("TLS client received alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription));
}
public override void NotifyServerVersion (ProtocolVersion serverVersion) {
base.NotifyServerVersion(serverVersion);
// Debug.LogError("TLS client negotiated " + serverVersion);
}
public override TlsAuthentication GetAuthentication () {
return new MyTlsAuthentication(mContext);
}
private class MyTlsAuthentication : TlsAuthentication {
private readonly TlsContext mContext;
internal WebuSocketTlsAuthentication (TlsContext context) {
this.mContext = context;
}
public void NotifyServerCertificate (Certificate serverCertificate) {
// X509CertificateStructure[] chain = serverCertificate.GetCertificateList();
// Console.WriteLine("TLS client received server certificate chain of length " + chain.Length);
// for (int i = 0; i != chain.Length; i++) {
// X509CertificateStructure entry = chain[i];
// // TODO Create fingerprint based on certificate signature algorithm digest
// Console.WriteLine(" fingerprint:SHA-256 " + TlsTestUtilities.Fingerprint(entry) + " (" + entry.Subject + ")");
// }
// なんもしてない。certが正しいかどうか、チェックしないといけないはず。
}
public TlsCredentials GetClientCredentials (CertificateRequest certificateRequest) {
byte[] certificateTypes = certificateRequest.CertificateTypes;
if (certificateTypes == null || !Arrays.Contains(certificateTypes, ClientCertificateType.rsa_sign)) {
return null;
}
// return TlsTestUtilities.LoadSignerCredentials(mContext, certificateRequest.SupportedSignatureAlgorithms, SignatureAlgorithm.rsa, "x509-client.pem", "x509-client-key.pem");
return null;
}
}
public override void NotifyHandshakeComplete () {
base.NotifyHandshakeComplete();
TlsSession newSession = mContext.ResumableSession;
if (newSession != null) {
// byte[] newSessionID = newSession.SessionID;
// string hex = Hex.ToHexString(newSessionID);
// if (this.mSession != null && Arrays.AreEqual(this.mSession.SessionID, newSessionID)) {
// Debug.LogError("Resumed session: " + hex);
// } else {
// Debug.LogError("Established session: " + hex);
// }
this.mSession = newSession;
}
Debug.LogError("handshake終わった、このへんが非同期に呼ばれるっぽいので、WebSocketのhandshakeを呼ぶきっかけにできるかもしれない。");
}
}
// client connection starts from here.
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9000);
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// connect socket to server peer.
socket.Connect(endPoint.Address, endPoint.Port);
/*
ready for tls handshake.
tlsClientProtocol contans the state machine,
which can generate "byte[] data" for negotiating TSL protocol with server.
*/
var tlsClientProtocol = new TlsClientProtocol(new SecureRandom());
// "Connect" means not connect to server. connect means initalize tlsClientProtocol in non-blocking mode.
tlsClientProtocol.Connect(new MyTlsClient(null));
// first, send ClientHello to server.
{
// get ClientHello byte data from tlsClientProtocol instance and send it to server.
var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()];
tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length);
// send it to server through socket.
socket.Send(buffer);
}
// wait response data from server.
{
while (socket.Available == 0) {}
var available = socket.Available;
var responseFromServer = new byte[available];
socket.Receive(responseFromServer);
// set received data to tlsClientProtocol by "OfferInput" method.
// tls handshake phase will progress.
tlsClientProtocol.OfferInput(responseFromServer);
// and next handshake data can be get from tlsClientProtocol.
var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()];
tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length);
// send it to server.
socket.Send(buffer);
}
// wait response data from server.
{
while (socket.Available == 0) {}
var available = socket.Available;
var assumedBuffer = new byte[available];
socket.Receive(assumedBuffer);
// set received data to tlsClientProtocol by "OfferInput" method.
// tls handshake phase will become completed! in this client side.
tlsClientProtocol.OfferInput(assumedBuffer);
// send ClientFinish data to server.
var buffer = new byte[tlsClientProtocol.GetAvailableOutputBytes()];
tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length);
socket.Send(buffer);
}
// tls session is established! yay!!
// sending data.
{
var helloString = "hello, tls.";
var helloBytes = Encoding.UTF8.GetBytes(helloString);
tlsClientProtocol.OfferOutput(helloBytes, 0, helloBytes.Length);
var count = tlsClientProtocol.GetAvailableOutputBytes();
var buffer = new byte[count];
tlsClientProtocol.ReadOutput(buffer, 0, buffer.Length);
socket.Send(buffer);
}
// receiving data.
// assume that server returns client sended data like echo.
{
while (socket.Available == 0) {}
var available = socket.Available;
var receiveBuffer = new byte[available];
socket.Receive(receiveBuffer);
tlsClientProtocol.OfferInput(receiveBuffer);
var len = tlsClientProtocol.GetAvailableInputBytes();
var receivedDecryptedBytes = new byte[len];
tlsClientProtocol.ReadInput(receivedDecryptedBytes, 0, receivedDecryptedBytes.Length);
Debug.LogError("server returned:" + Encoding.UTF8.GetString(receivedDecryptedBytes));
}
というわけで使い方がわかったので、WebSocketのHandShakeまで完成
で、WebuSocketでは不完全なデータが来た場合、それを破棄したい。
不完全なデータ = 途中で途切れて送られてくるデータ。
WebuSocketでは、すでにC#のSocketArgsのバッファがあるので、そのバッファにデータを溜めたまま、
データ全体がWebSocketのframeとして読めるようになるまで読み出さないという戦略を取っている。
で、これと、bcのTlsProtocolの実装との相性が悪い。
tlsProtocol、内部で完全にデータをコピーして動いている。
データを読み込ませると、それはinputStreamに突っ込まれる。で、中途半端なデータが来た場合、TLSの復号に失敗し、データは内部に残る。
bcとしては、そこに新しいデータを足すだけで動くようにしたいらしい。
で、WebuSocketとしてはそのキャッシュ機構が邪魔で、そいつを破棄したい。できるのかな~~
サイズが0になったら、WebSocketのフレームも不足しているものとして扱うことはできる。
すでに入っているものを消せるか?
-> inputStreamのSkipがその用途に使えそうだけど、これうーーーんん、、、どこからも叩かれてる形跡が無い。
BouncyCastleを変更するの避けたいな、、、Discardとかもなさそうだしな、、
-> リセットを実装するの簡単だった。
内部に用意してあったinput用のstreamに対して、ClientTlsProtocolを継承することで追加したメソッドからinputStream.Skip(N)を呼ぶことで対処できた。
で、次の戦略が成立する。
1.長さが足りないデータが来た場合、TLSProtocolを通すと、available = 0が返ってくる
2.availableが0の場合、そのデータはTLS的にダメなんで、WebSocketのframeとしても認めないことにする。
-> tls inputをリセットした上で、再度データが来るのを待つ。
3.availableが0でないデータの場合、TLS的にはOKなんで、復号できたデータをws frameとして展開しようとする。
4.wsのframe展開に失敗した場合、展開成功するまでデータはバッファに残った状態になる。
-> tls inputをリセットする必要は無いはず。次来たデータについての処理は、1から実行されるので。
本当は、bcの中でキャッシュするのもやめさせたい。どこまで読んだかっていうインデックスを操れるといいんだけど。
内部に一度コピーが走っちゃうのは本当に辛いな~~~~~~~~~
-> このへんも書き換えられそう。いいね!!
教訓
むやみにinternalを使いまくらないというBouncyCastleの人らの方針のおかげで欲しいもの作れた。
なんでドキュメントがないんだろう。悲しい。
あと、なんでbyteを吐き出すようなTls state machine がC#標準に無いの?
SslStreamだけで満足なの?
、、という疑問は誰かあの、、ぜひ、、教えてください、、、
Task使わずにそれらをコントロールできるAPIがスッゲー貧弱なんで、thread一切作らずパフォーマンス欲しい時に辛かった。
thread一つも作らずに済ませられるならそっちのほうがよくない?